package org.amphiprion.apiforanywebsite.engine;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;

import org.amphiprion.apiforanywebsite.annotation.WebElement;
import org.amphiprion.apiforanywebsite.annotation.WebElementRoot;
import org.amphiprion.apiforanywebsite.exception.ParserException;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class WebMarshaller<T> {
	private Class rootClass;

	public WebMarshaller(Class<T> rootClass) {
		this.rootClass = rootClass;
	}

	public List<T> marshall(Document document) {
		List<T> result = new ArrayList<T>();
		// System.out.println(document.outerHtml());
		try {

			WebElementRoot anno = (WebElementRoot) rootClass.getAnnotation(WebElementRoot.class);
			// System.out.println("root=" + anno);
			if (anno != null) {
				String selector = anno.cssSelector();
				Elements elts = null;
				if (selector == null || selector.isEmpty()) {
					elts = document.children();
				} else {
					// System.out.println("selector=" + selector);
					elts = document.select(selector);
				}
				// System.out.println("   elts=" + elts.size());
				for (Element elt : elts) {
					Object root = rootClass.newInstance();
					// System.out.println("newInstance:" + root.getClass());
					// System.out.println(elts.outerHtml());
					parseClass(root, elt);
					result.add((T) root);
				}
			}

		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (Throwable e) {
			e.printStackTrace();
		}

		return result;
	}

	private void parseClass(Object classToParse, Element docElement) throws InstantiationException, IllegalAccessException, ClassNotFoundException, ParserException, NumberFormatException, IllegalArgumentException, InvocationTargetException {
		// System.out.println(docElement.html());
		// System.out.println(pad + "--------------");
		{
			Field[] fields = classToParse.getClass().getDeclaredFields();
			for (Field field : fields) {
				WebElement anno = field.getAnnotation(WebElement.class);
				if (anno != null) {
					String selector = anno.cssSelector();
					Elements elts = null;
					if (selector == null || selector.isEmpty()) {
						elts = docElement.children();
					} else {
						// System.out.println(pad + "selector=" + selector);
						elts = docElement.select(selector);
					}
					// System.out.println(pad + "elts=" + elts.size());
					// System.out.println(pad + "fieldName=" + field);
					if (field.getType().isAssignableFrom(List.class)) {
						// list element
						Type ptype = field.getGenericType();
						if (!(ptype instanceof ParameterizedType)) {
							throw new ParserException("You must use parametrized list instead of " + ptype + " for field " + field);
						}
						Class listElement = (Class) ((ParameterizedType) ptype).getActualTypeArguments()[0];

						ArrayList ol = new ArrayList();
						// Object ol = field.getType().newInstance();
						// System.out.println(pad + "newInstance:" +
						// ol.getClass());

						field.setAccessible(true);
						field.set(classToParse, ol);

						for (Element elt : elts) {
							Object listItem = listElement.newInstance();
							// System.out.println(pad + "newInstance:" +
							// listItem.getClass());
							((List) ol).add(listItem);
							parseClass(listItem, elt);
						}
					} else {
						if (elts.size() > 1) {
							System.err.println("Only a single occurence must exists instead of " + elts.size() + " for the field " + field + ", so the first one is used.");
						}
						if (elts.size() >= 1) {
							Element elt = elts.get(0);
							Object data = getData(elt, anno, field.getType());
							field.setAccessible(true);
							field.set(classToParse, data);
						}
					}
				}
			}
		}
		// Methods
		Method[] methods = classToParse.getClass().getDeclaredMethods();
		for (Method method : methods) {
			WebElement anno = method.getAnnotation(WebElement.class);
			if (anno != null) {
				String selector = anno.cssSelector();
				Elements elts = null;
				if (selector == null || selector.isEmpty()) {
					elts = docElement.children();
				} else {
					// System.out.println(pad + "selector=" + selector);
					elts = docElement.select(selector);
				}
				// System.out.println(pad + "elts=" + elts.size());
				// System.out.println(pad + "fieldName=" + field);
				if (method.getParameterTypes()[0].isAssignableFrom(List.class)) {
					// list element
					Type ptype = method.getGenericParameterTypes()[0];
					if (!(ptype instanceof ParameterizedType)) {
						throw new ParserException("You must use parametrized list instead of " + ptype + " in method " + method);
					}
					Class listElement = (Class) ((ParameterizedType) ptype).getActualTypeArguments()[0];

					// System.out.println(listElement);
					ArrayList ol = new ArrayList();
					// Object ol = field.getType().newInstance();
					// System.out.println(pad + "newInstance:" + ol.getClass());

					method.setAccessible(true);
					method.invoke(classToParse, ol);

					for (Element elt : elts) {
						Object listItem = listElement.newInstance();
						// System.out.println(pad + "newInstance:" +
						// listItem.getClass());
						((List) ol).add(listItem);
						parseClass(listItem, elt);
					}
				} else {
					if (elts.size() > 1) {
						throw new ParserException("Only a single occurence must exists instead of " + elts.size() + " for the method " + method);
					} else if (elts.size() == 1) {
						Element elt = elts.get(0);
						Object data = getData(elt, anno, method.getParameterTypes()[0]);
						method.setAccessible(true);
						method.invoke(classToParse, data);
					}
				}
			}
		}
	}

	private Object getData(Element elt, WebElement anno, Class dataType) throws ParserException {
		String data = null;
		switch (anno.valueType()) {
		case TEXT:
			data = elt.text();
			break;
		case OWN_TEXT:
			data = elt.ownText();
			break;
		case INNER_HTML:
			data = elt.html();
			break;
		case OUTER_HTML:
			data = elt.outerHtml();
			break;
		case ATTRIBUTE:
			data = elt.attr(anno.attributeName());
			break;
		default:
		}
		if (data == null) {
			return data;
		}
		Object o = null;
		if (dataType.isAssignableFrom(String.class)) {
			o = data.trim();
		} else if (dataType.isAssignableFrom(int.class)) {
			o = Integer.parseInt(data.trim());
		} else if (dataType.isAssignableFrom(long.class)) {
			o = Long.parseLong(data.trim());
		} else if (dataType.isAssignableFrom(float.class)) {
			o = Float.parseFloat(data.trim().replace(",", "."));
		} else if (dataType.isAssignableFrom(double.class)) {
			o = Double.parseDouble(data.trim().replace(",", "."));
		} else {
			throw new ParserException("The following type is not yet managed : " + dataType);
		}
		return o;
	}
}
